# All the special stuff related to authentication of users.
#
# The course evaluation site has special needs: Authentication against
# an external SSO database, and denying all access to everything but
# the login page as long as a valid login is not provided.

import socket
import urllib
import urllib2
import datetime

from django.shortcuts import render_to_response
from django import http
from django import forms
from django import conf
from django.core import validators
from django.core.urlresolvers import reverse
from django.contrib import auth
import django.contrib.auth.forms
import django.contrib.auth.models
import django.contrib.auth.backends

# Middleware that forces all incoming requests to render the login
# blinder until a valid login and password are provided to
# authenticate the user.
class AccessBlinderMiddleware:
  def process_request(self, request):
    # If we already have an authenticated user in the request, nothing
    # to do.
    if hasattr(request, 'user') and request.user.is_authenticated():
      return None

    # If we're in debug mode and serving something from the media dir,
    # don't object.
    if conf.settings.DEBUG and request.path.startswith('/site_media/'):
      return None

    # Get the login URL, either from the configuration (override of
    # the critic login servlet), or via a reverse lookup of the login
    # servlet.
    if not hasattr(self, 'login_url'):
      login_servlet = getattr(conf.settings, 'LOGIN_VIEW', None)
      if not login_servlet:
        return http.HttpResponseForbidden('Access denied.')

      self.login_url = reverse(login_servlet)

    # Anonymous users can only go to /login. Let them through if
    # they're headed there, redirect them if not.
    if request.path == self.login_url:
      return None

    if request.path != '/':
      return http.HttpResponseRedirect(
        '%s?continue=%s' % (self.login_url, request.path))
    else:
      return http.HttpResponseRedirect(self.login_url)

class AuthenticatorAdminBypass(auth.backends.ModelBackend):
  """Perform admin authentication against the local user store.

  This class provides a way for site staff to log in even if the SSO
  infrastructure is unavailable. They should prefix their SSO username
  with '@@', and use their admin password (not SSO password). This
  module will then try to log them in as """

  def authenticate(self, username=None, password=None):
    if username.startswith('@@'):
      username = username[2:]
      try:
        user = auth.models.User.objects.get(username=username)
        if user.is_staff and user.check_password(password):
          return user
      except auth.models.User.DoesNotExist:
        pass
    return None

class AuthenticatorSingleSignOn:
  """Perform user authentication against an AE SSO server.

  This authenticator is here to tie the application into the accounts
  database of the AE. The AE SSO server API lets us test whether a
  user/password combination is valid.

  For valid logins, we create a mirror object in our own user store,
  which the application will then kick around. All authentication
  always goes to the SSO server though.
  """

  def _connect(self, api_key, username, password, mode='utbm'):
    """Use urlopen to get a handle to the authentication server.

    Note that we have a large kludgy hack here, to work around the
    fact that urllib2 applies an infinite timeout to the underlying
    socket. That's bad, so we override the default timeout of the
    socket lib while making the urlopen request.
    """
    data = urllib.urlencode({
      'api_key': api_key,
      'mode': mode,
      'username': username,
      'password': password,
      })
    global_timeout = socket.getdefaulttimeout()
    socket.setdefaulttimeout(3.0) # timeout after 3 seconds
    resp = urllib2.urlopen(conf.settings.SSO_AUTH_BACKEND_URL, data)
    socket.setdefaulttimeout(global_timeout)
    return resp

  def authenticate(self, username=None, password=None):
    if not (username and password):
      return None

    # If we are in dev mode, use the fake auth logic
    if conf.settings.SSO_AUTH_API_KEY == 'DEV':
      if username != password:
        return None
    # Using a production API key, shoot the request over to the authn
    # service.
    else:
      try:
        resp = self._connect(api_key=conf.settings.SSO_AUTH_API_KEY,
                             username=username, password=password)
        resp_len = resp.info().getheader('Content-Length', 0)
        if resp_len not in ('1', '2'):
          # The response is not the right size, abort.
          resp.close()
          return None
        auth_response = resp.read(int(resp_len))
        resp.close()
      except:
        auth_response = '-1'

      if auth_response == '-1':
        raise validators.ValidationError(_("Serveur d'authentification "
                                           "temporairement indisponible. "
                                           "Veuillez reessayer plus tard."))
      elif auth_response != '1':
        return None

    # Auth successful, try to get the associated user
    try:
      user = auth.models.User.objects.get(username=username)
    except auth.models.User.DoesNotExist:
      now = datetime.datetime.now()
      user = auth.models.User.objects.create(username=username,
                                             password='AE-SSO',
                                             email=username+'@utbm.fr',
                                             is_active=True,
                                             is_staff=False,
                                             is_superuser=False,
                                             last_login=now,
                                             date_joined=now)

    return user

  def get_user(self, user_id):
    try:
      return auth.models.User.objects.get(pk=user_id)
    except auth.models.User.DoesNotExist:
      return None
